import { my, Storage, BaseContract } from '@antchain/myassembly';
import { Address, Attribute, AttributeType, Collection } from './types';
import { parseHex2Int, stringifyCollection, stringifyCollections } from './utils';

export default class Contract extends BaseContract {
  // You can use this hash to verify the rule image file containing all the fish's attributes
  // $ md5 ./assets/rule.png => ...  
  //TODO 资源图标待完善
  public ruleHash: string = '';
  // RestartCampusLife standard name
  public standard: string = 'RestartCampusLife';

  // Collection count limit per address
  private limit: u32;
  // Mint available, depend on developer
  private canMint: Storage<bool> = new Storage('canMint', true);
  // CryptoFish contract owner's address
  private owner: Storage<Address> = new Storage('owner', '');
  // Collections list
  private collections: Storage<Collection[]> = new Storage('collections', []);
  private collectionAttributeMap: Storage<Map<Attribute, bool>> = new Storage(
    'collectionAttributeMap',
    new Map<Attribute, bool>(),
  );
  // Picked logo collection
  private logo: Storage<Collection> = new Storage('logo', new Map<string, string>());
  private isLogoPicked: Storage<bool> = new Storage('isLogoPicked', false);

  // Private builtin attributes infos
  // attribute key list depends on rule hash
  private attributeKeyList: AttributeType[] = ['head', 'body', 'leg', 'feet'];
  private attributeWeights: Map<AttributeType, u32> = new Map<AttributeType, u32>();
  private attributes: Map<AttributeType, string> = new Map<AttributeType, string>();

  constructor () {
    super();

    this.limit = 1


    this.attributes.set('head','0123456789ABCDEF')
    this.attributes.set('body','0123456789ABCDEF')
    this.attributes.set('leg','0123456789ABCDEF')
    this.attributes.set('feet','0123456789ABCDEF')
  }

  @EXPORT
  public onContractDeploy(): void{
    if (this.owner.getData().length > 0) {
      throw new Error('[RestartCampusLife] cannot run this method');
    }

    const ownerAddress = my.getSender.toString();
    this.owner.setData(ownerAddress);
    this.log(`contract created by: ${ownerAddress}`)

    this.setCanMint(1)

  }

  @EXPORT
  public mint(): string{
    const creator = my.getSender().toString();
    const ownedCount = <u32>this.getOwnedCollectionsPrivate().length;
    const canMint = this.canMint.getData();
    if (ownedCount >= this.limit) {
      this.log(`error: You have already registered!`);
      return 'null';
    }

    if (!canMint) {
      this.log(`error: ${this.standard} minting is not available`);
      return 'null';
    }


    const attribute = this.generateUniqAttribute();
    const collections = this.collections.getData();
    const collectionAttributeMap = this.collectionAttributeMap.getData();
    const index = collections.length;

    const collection: Collection = new Map<string, string>();
    collection.set('index', index.toString());
    collection.set('creator', creator);
    collection.set('attribute', attribute);


    this.log('registered success:');
    this.printCollection(collection);

    // save to chain
    collections.push(collection);
    this.collections.setData(collections);
    // mark attribute
    collectionAttributeMap.set(attribute, true);
    this.collectionAttributeMap.setData(collectionAttributeMap);
    return stringifyCollection(collection);
  }

  private generateUniqAttribute(): Attribute {
    const txHash = my.getTxHash();
    const headRange = this.attributes.get('head').length;
    const bodyRange = this.attributes.get('body').length;
    const legRange = this.attributes.get('leg').length;
    const feetRange = this.attributes.get('feet').length;


    for (let index = 0; index < txHash.length - 6; index += 1) {
      const sourceAttribute = txHash.substr(index, 4).split('');
      const head = parseHex2Int(sourceAttribute[0]) % headRange;
      const body = parseHex2Int(sourceAttribute[1]) % bodyRange;
      const leg = parseHex2Int(sourceAttribute[2]) % legRange;
      const feet = parseHex2Int(sourceAttribute[3]) % feetRange;
    
      const attribute = `${head.toString()}${body.toString()}${leg.toString()}${feet.toString()}`;
      if (this.isAttributeAvailable(attribute)) return attribute;
    }
    throw new Error('[RestartCampusLife] cannot generate attribute');
  }

  private isAttributeAvailable(attribute: Attribute): bool {
    // Should be unique
    const collectionAttributeMap = this.collectionAttributeMap.getData();
    if (collectionAttributeMap.has(attribute) && collectionAttributeMap.get(attribute) === true) {
      return false;
    }

    // Should be contained in attributes range

    // Congratulations! Your generated attribute is available.
    return true;
  }

    // Get picked logo collection
    @EXPORT
    public getLogo(): string {
      const isLogoPicked = this.isLogoPicked.getData();
      if (!isLogoPicked) {
        throw new Error('[cryptofish] logo has not been picked, please wait');
      }
      const logo = this.logo.getData();
      this.log('get logo success:');
      this.printCollection(logo);
      return stringifyCollection(logo);
    }
  
    // Favor collection by #Index
    // "favorByIndex(int)[0]" => "true/false"
    @EXPORT
    public favorByIndex(index: u32): bool {
      return this.favorCollectionByIndex(index, this.getCollectionByIndexPrivate(index));
    }
  
    // Favor collection by attribute
    // "favorByAttribute(string)[123456]" => "true/false"
    @EXPORT
    public favorByAttribute(attribute: string): bool {
      const collections = this.collections.getData();
      let idx!: u32;
      let collection!: Collection;
      for (let index = 0; index < collections.length; index += 1) {
        const current = collections[index];
        if (current.get('attribute') == attribute) {
          idx = index;
          collection = current;
          break;
        }
      }
      return this.favorCollectionByIndex(idx, collection);
    }
  
    // Favor collection by attribute
    private favorCollectionByIndex(index: u32, collection: Collection): bool {
      if (!collection.get('index')) return false;
      // current address
      const address = my.getSender().toString();
  
      // cannot favor your owned collection, except developer
      if (collection.get('creator') == address && !this.isOwner()) return false;
      const favorCount = parseInt(collection.get('favorCount'), 10);
      collection.set('favorCount', (<u32>(favorCount + 1)).toString());
      this.updateCollectionByIndex(index, collection);
      return true;
    }
  
    private updateCollectionByIndex(index: u32, collection: Collection): void {
      const collections = this.collections.getData();
      collections[<i32>index] = collection;
      this.collections.setData(collections);
    }
  
    // Get cryptofish collection by index(u32)
    // "getCollectionByIndex(int)[1]" => "collection(Map<string, string>)"
    @EXPORT
    public getCollectionByIndex(index: u32): string {
      return stringifyCollection(this.getCollectionByIndexPrivate(index));
    }
    public getCollectionByIndexPrivate(index: u32): Collection {
      const collections = this.collections.getData();
      const collection = collections[index];
      this.log(`getCollectionByIndex(${index}) =>`);
      this.printCollection(collection);
      return collection;
    }
  
    // Get cryptofish collection by attribute(string)
    // "getCollectionByAttribute(string)[123456]" => "collection(Map<string, string>)"
    @EXPORT
    public getCollectionByAttribute(attribute: string): string {
      return stringifyCollection(this.getCollectionByAttributePrivate(attribute));
    }
    @EXPORT
    public getCollectionByAttributePrivate(attribute: string): Collection {
      const collections = this.collections.getData();
      let collection!: Collection;
      for (let index = 0; index < collections.length; index += 1) {
        const current = collections[index];
        if (current.get('attribute') == attribute) {
          collection = current;
          break;
        }
      }
      this.log(`getCollectionByAttribute(${attribute}) =>`);
      this.printCollection(collection);
      return collection;
    }
  
    // Get owned collections
    // "getOwnedCollections()" => "collection[](Array<Map<string, string>>)"
    @EXPORT
    public getOwnedCollections(): string {
      return stringifyCollections(this.getOwnedCollectionsPrivate());
    }
    @EXPORT
    public getOwnedCollectionsPrivate(): Collection[] {
      const address: string = my.getSender().toString();
      const totalCollections = this.collections.getData();
      const collections: Collection[] = [];
      for (let index = 0; index < totalCollections.length; index += 1) {
        const collection = totalCollections[index];
        if (collection.get('creator') != address) continue;
        collections.push(collection);
      }
      this.log(`getOwnedCollections: ${collections.length}`);
      this.printCollections(collections);
      return collections;
    }

  @EXPORT
    public getCollectionCount(): u32 {
      return <u32>this.collections.getData().length;
    }
  
    // Get all collections with limit and skip filter
    // "getCollections(int, int)[20, 0]" => "collection[](Array<Map<string, string>>)"
    @EXPORT
    public getCollections(limit: u32, skip: u32): string {
      return stringifyCollections(this.getCollectionsPrivate(limit, skip));
    }
    @EXPORT
    public getCollectionsPrivate(limit: u32, skip: u32): Collection[] {
      const collections = this.collections.getData();
      const collectionsPart = collections.slice(skip, skip + limit);
      this.printCollections(collectionsPart);
      return collectionsPart;
    }
  

  
    // Calculate score by attribute
    private calculateScore(attribute: Attribute): u32 {
      const attrStrList: string[] = attribute.split('');
      const attrU32List: u32[] = attrStrList.map<u32>((hex) => parseHex2Int(hex));
      let score: u32 = 0;
  
      // Prevent to use `reduce` because of the closures issue in assemblyscript
      for (let index = 0; index < attrU32List.length; index++) {
        const currentScore = attrU32List[index];
        const weight = this.attributeWeights.get(this.attributeKeyList[index]) || 100;
        score += currentScore * weight;
      }
      return score;
    }
  


    @EXPORT
  public setCanMint(canMint: u32): void {
    if (this.isOwner()) {
      this.canMint.setData(canMint === 0 ? false : true);
    }
  }

  private isOwner(): bool {
    return this.owner.getData() == my.getSender().toString();
  }

    // Print collections to stdout
    private printCollections(c: Collection[]): void {
      for (let index = 0; index < c.length; index += 1) {
        this.printCollection(c[index]);
      }
    }
  
    // Print collection to stdout
    private printCollection(c: Collection): void {
      const index = c.get('index');
      if (!index) {
        this.log('error: collection not found');
      }
      const creator = c.get('creator');
      const attribute = c.get('attribute');
      this.log(
        `Collection{index:${index}, creator:"${creator}", attribute:"${attribute}"}`,
      );
    }



  private log(message: string): void {
    my.log(`[RestartCampusLife] ${message}`, []);
  }


}
